﻿using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Linq;

namespace Babylon.Importers
{
    public partial class ObjImporter : IModelImporter
    {
        ModelMesh<VertexPositionNormalTextureTangent> currentMesh;
        ModelMeshPart currentPart;
        readonly List<Vector3> positions = new List<Vector3>();
        readonly List<Vector3> normals = new List<Vector3>();
        readonly List<Vector2> textureCoordinates = new List<Vector2>();
        readonly List<VertexPositionNormalTextureTangent> stagingVertices = new List<VertexPositionNormalTextureTangent>();
        int positionsIndexOffset = 1;
        int normalsIndexOffset = 1;
        int textureCoordinatesIndexOffset = 1;
        int currentIndex;

        SynchronizationContext synchronizationContext;

        public event Action<Model> OnImportCompleted;
        public event Action<int> OnImportProgressChanged;

        public void ImportAsync(Stream sourceStream, Func<string, Stream> resourceLocator, GraphicsDevice device, ImportationOptions importationOptions = ImportationOptions.None)
        {
            synchronizationContext = SynchronizationContext.Current;


            Task<Model> task = Task.Factory.StartNew(() =>
            {
                Document<ObjLine> objDocument = new Document<ObjLine>(sourceStream);

                Model result = new Model(device);

                effects.Clear();

                int currentProgression = 0;

                int total = objDocument.Blocks.Sum(lines => lines.Count);

                foreach (var lines in objDocument.Blocks)
                {
                    if (lines[0].Tokens[0] == lines[0].BlockSperator)
                        AppendNewMesh(result, importationOptions);
                    foreach (ObjLine line in lines)
                    {
                        currentProgression++;
                        ReportProgressChanged(50 + (currentProgression * 50) / total);

                        switch (line.Header)
                        {
                            case ObjHeader.Vertices:
                                positions.Add(line.ToVector3());
                                break;
                            case ObjHeader.TextureCoordinates:
                                Vector2 tv = line.ToVector2();
                                tv.Y = 1.0f - tv.Y;

                                textureCoordinates.Add(tv);
                                break;
                            case ObjHeader.Normals:
                                normals.Add(line.ToVector3());
                                break;
                            case ObjHeader.Group:
                                break;
                            case ObjHeader.Faces:
                                if (currentPart == null || currentPart.IsFrozen)
                                    AppendNewPart(null, device);
                                AppendFace(line);
                                break;
                            case ObjHeader.MaterialLibrary:
                                ImportMaterialLibraries(line, resourceLocator, device);
                                break;
                            case ObjHeader.Material:
                                AppendNewPart(line, device);
                                break;
                        }
                    }
                }

                FreezeCurrentMesh();
                result.Freeze();

                return result;
            });

            task.ContinueWith(t =>
                                  {
                                      if (OnImportCompleted != null)
                                          OnImportCompleted(t.Result);
                                  }, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
        }

        void ReportProgressChanged(int progress)
        {
            synchronizationContext.Post(state =>
                                            {
                                                if (OnImportProgressChanged != null)
                                                {
                                                    OnImportProgressChanged(progress);
                                                }
                                            }, null);
        }

        void AppendNewPart(ObjLine line, GraphicsDevice device)
        {
            if (currentPart != null)
            {
                currentPart.Freeze();
            }
            
            currentPart = new ModelMeshPart(currentMesh);

            if (line != null)
            {
                string materialName = line.Tokens[1];
                currentPart.Effect = effects.Where(e => e.Name == materialName).FirstOrDefault();
            }
            else
            {
                currentPart.Effect = new BasicEffect(device);
            }

            currentPart.PrimitivesCount = 0;
            currentPart.NumVertices = 0;
            currentPart.StartIndex = currentIndex;
            currentPart.MinVertexIndex = int.MaxValue;
        }

        void AppendNewMesh(Model result, ImportationOptions importationOptions)
        {
            FreezeCurrentMesh();
            currentMesh = new ModelMesh<VertexPositionNormalTextureTangent>(result, (importationOptions & ImportationOptions.Optimize) == ImportationOptions.Optimize);

            currentIndex = 0;
        }

        void AppendFace(ObjLine line)
        {
            // Line
            if (line.Tokens.Length == 3)
            {
                AppendIndex(1, line);
                AppendIndex(2, line);
                AppendIndex(2, line);

                currentPart.PrimitivesCount++;
                return;
            }

            // Triangle
            if (line.Tokens.Length == 4)
            {
                for (int index = 1; index <= 3; index++)
                {
                    AppendIndex(index, line);
                }

                currentPart.PrimitivesCount++;
                return;
            }

            // Quad
            if (line.Tokens.Length == 5)
            {
                for (int index = 1; index <= 3; index++)
                {
                    AppendIndex(index, line);
                }
                currentPart.PrimitivesCount++;

                AppendIndex(1, line);
                AppendIndex(3, line);
                AppendIndex(4, line);
                currentPart.PrimitivesCount++;
            }
        }

        void AppendIndex(int index, ObjLine line)
        {
            string[] indices = line.Tokens[index].Split('/');

            // Required: Position
            int positionIndex = int.Parse(indices[0], CultureInfo.InvariantCulture) - positionsIndexOffset;
            int texCoordIndex = -1;
            int normalIndex = -1;

            // Optional: Texture coordinate
            if (indices.Length > 1 && indices[1].Equals(string.Empty) == false)
            {
                texCoordIndex = int.Parse(indices[1]) - textureCoordinatesIndexOffset;
            }

            // Optional: Normal
            if (indices.Length > 2 && indices[2].Equals(string.Empty) == false)
            {
                normalIndex = int.Parse(indices[2]) - normalsIndexOffset;
            }

            // Build vertex
            VertexPositionNormalTextureTangent vertex = new VertexPositionNormalTextureTangent { Position = positions[positionIndex] };

            if (texCoordIndex >= 0)
                vertex.TextureCoordinates = textureCoordinates[texCoordIndex];

            if (normalIndex >= 0)
                vertex.Normal = normals[normalIndex];

            // check if the vertex does not already exists

            stagingVertices.Add(vertex);
            currentMesh.AddVertex(vertex);

            int vertexID = stagingVertices.Count - 1;

            currentPart.NumVertices++;

            // Register index
            currentMesh.AddIndex(vertexID);
            currentIndex++;

            if (vertexID < currentPart.MinVertexIndex)
                currentPart.MinVertexIndex = vertexID;
        }

        void FreezeCurrentMesh()
        {
            positionsIndexOffset += positions.Count;
            positions.Clear();

            normalsIndexOffset += normals.Count;
            normals.Clear();

            textureCoordinatesIndexOffset += textureCoordinates.Count;
            textureCoordinates.Clear();
            stagingVertices.Clear();

            if (currentPart != null)
                currentPart.Freeze();

            if (currentMesh == null)
                return;

            currentMesh.Freeze(ComputeTangents);
        }

        void ComputeTangents(IList<VertexPositionNormalTextureTangent> vertices, IList<int> indices)
        {
            Vector3[] tan = new Vector3[vertices.Count];

            int facesCount = indices.Count / 3;
            for (int index = 0; index < facesCount; index++)
            {
                int i1 = indices[index * 3];
                int i2 = indices[index * 3 + 1];
                int i3 = indices[index * 3 + 2];

                VertexPositionNormalTextureTangent v1 = vertices[i1];
                VertexPositionNormalTextureTangent v2 = vertices[i2];
                VertexPositionNormalTextureTangent v3 = vertices[i3];

                Vector2 w1 = v1.TextureCoordinates;
                Vector2 w2 = v2.TextureCoordinates;
                Vector2 w3 = v3.TextureCoordinates;

                float x1 = v2.Position.X - v1.Position.X;
                float x2 = v3.Position.X - v1.Position.X;
                float y1 = v2.Position.Y - v1.Position.Y;
                float y2 = v3.Position.Y - v1.Position.Y;
                float z1 = v2.Position.Z - v1.Position.Z;
                float z2 = v3.Position.Z - v1.Position.Z;

                float s1 = w2.X - w1.X;
                float s2 = w3.X - w1.X;
                float t1 = w2.Y - w1.Y;
                float t2 = w3.Y - w1.Y;

                float r = 1.0F / (s1 * t2 - s2 * t1);

                Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);

                tan[i1] += sdir;
                tan[i2] += sdir;
                tan[i3] += sdir;
            }

            for (int index = 0; index < vertices.Count; index++)
            {
                VertexPositionNormalTextureTangent vertex = vertices[index];

                vertex.Tangent = Vector3.Normalize(tan[index] - vertex.Normal * Vector3.Dot(vertex.Normal, tan[index]));

                vertices[index] = vertex;
            }
        }
    }
}
